﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

namespace MyLib.Data {
	/// <summary>
	/// 
	/// </summary>
	public class AutoCompleteProvider : FileContextProvider {
		/// <summary>
		/// 
		/// </summary>
		public class BagItem {
			/// <summary>
			/// 
			/// </summary>
			public string[] Keys { get; private set; }

			/// <summary>
			/// 
			/// </summary>
			public NameObjectCollection Properties { get; private set; }

			/// <summary>
			/// 
			/// </summary>
			/// <param name="keys"></param>
			public BagItem(string[] keys)
				: this(keys, null) {
			}

			/// <summary>
			/// 
			/// </summary>
			/// <param name="name"></param>
			/// <returns></returns>
			public object this[string name] {
				get {
					return this.Properties[name];
				}
				set {
					this.Properties[name] = value;
				}
			}

			/// <summary>
			/// 
			/// </summary>
			/// <param name="keys"></param>
			/// <param name="properties"></param>
			public BagItem(string[] keys, NameObjectCollection properties) {
				System.Diagnostics.Debug.Assert(keys != null);
				System.Diagnostics.Debug.Assert(keys.Length > 0);

				Keys = keys;

				if (properties != null)
					Properties = properties;
				else
					Properties = new NameObjectCollection();
			}
		}

		/// <summary>
		/// 
		/// </summary>
		private const string DEFAULT_DATA_FILE_NAME = @"AutoComplete.xml";

		/// <summary>
		/// 
		/// </summary>
		private const string XSD_FILE_NAME = "AutoComplete.xsd";

		/// <summary>
		/// 
		/// </summary>
		private const string XML_BAG_NAME = "bag";

		/// <summary>
		/// 
		/// </summary>
		private const string XML_ITEM_KEY_NAME = "key";

		/// <summary>
		/// 
		/// </summary>
		private const string KEYS_SEPERATOR = ",";

		/// <summary>
		/// 
		/// </summary>
		private const string XML_ITEM_NAME = "item";

		/// <summary>
		/// 
		/// </summary>
		private const string XML_PROPERTY_NAME = "property";

		/// <summary>
		/// 
		/// </summary>
		private const string XML_PROPERTY_NAME_NAME = "name";

		/// <summary>
		/// 
		/// </summary>
		private const string XML_PROPERTY_VALUE_NAME = "value";

		/// <summary>
		/// 
		/// </summary>
		private const string XML_PROPERTY_TYPE_NAME = "type";

		/// <summary>
		/// 
		/// </summary>
		private string m_bagFileName;

		/// <summary>
		/// 
		/// </summary>
		private string m_cacheKey;

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public IEnumerable<BagItem> LoadBag() {
			IEnumerable<BagItem> bag = null;

			if (CacheProvider != null) {
				bag = CacheProvider[m_cacheKey] as IEnumerable<BagItem>;
			}

			if (bag == null) {
				bag = LoadBagFromFile(m_bagFileName);

				if (CacheProvider != null) {
					CacheProvider.Add(m_cacheKey, bag, m_bagFileName);
				}
			}

			return bag;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="bag"></param>
		public void SaveBag(IEnumerable<BagItem> bag) {
			System.Diagnostics.Debug.Assert(bag != null);

			if (CacheProvider != null) {
				CacheProvider.Add(m_cacheKey, bag, m_bagFileName);
			}

			SaveBagToFile(m_bagFileName, bag);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="key"></param>
		/// <returns></returns>
		public NameObjectCollection GetItemByKey(string key) {
			var bag = LoadBag();

			return bag
				.Where(_i => _i.Keys.Contains(key))
				.Select(_i => _i.Properties)
				.FirstOrDefault();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="bagFileName"></param>
		/// <returns></returns>
		private static IEnumerable<BagItem> LoadBagFromFile(string bagFileName) {
			// Prepare schema.
			var schemas = new XmlSchemaSet();
			var xss = typeof(CodeProvider)
				.Assembly
				.GetManifestResourceStream("MyLib." + XSD_FILE_NAME); // Use internal schema.
			using (var xsr = XmlReader.Create(xss)) {
				schemas.Add(null, xsr);
			}

			if (!System.IO.File.Exists(bagFileName)) {
				yield break;
			}

			// Load code file.
			XElement root;
			using (var bagFileStream = new FileStream(
				bagFileName,
				FileMode.Open,
				FileAccess.Read)) {

				root = XmlExtensions.LoadXElement(bagFileStream, schemas);

				bagFileStream.Close();
			}

			foreach (var itemElement in root.Elements(XML_ITEM_NAME)) {
				var keyArray = itemElement.Attribute(XML_ITEM_KEY_NAME).Value.Split(
					new string[] { KEYS_SEPERATOR },
					StringSplitOptions.RemoveEmptyEntries);

				if (keyArray.Length == 0)
					continue;

				for (int i = 0; i < keyArray.Length; ++i) {
					keyArray[i] = keyArray[i].Trim();
				}

				var propDict = new NameObjectCollection();
				foreach (var propElement in itemElement.Elements(XML_PROPERTY_NAME)) {
					var name = propElement.Attribute(XML_PROPERTY_NAME_NAME).Value;

					var valueAttr = propElement.Attribute(XML_PROPERTY_VALUE_NAME);
					if (valueAttr != null) {
						var typeName = propElement.Attribute(XML_PROPERTY_TYPE_NAME).Value;
						propDict[name] = GetValueFromValueString(valueAttr.Value, typeName);
					}
				} // end of foreach.

				yield return new BagItem(keyArray, propDict);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="bagFileName"></param>
		/// <param name="bag"></param>
		private static void SaveBagToFile(string bagFileName, IEnumerable<BagItem> bag) {
			using (var stream = new FileStream(bagFileName, FileMode.Create, FileAccess.Write)) {
				var writer = new XmlTextWriter(stream, Encoding.UTF8);
				writer.Indentation = 4;
				writer.IndentChar = ' ';
				writer.Formatting = Formatting.Indented;
				writer.WriteStartDocument(false);

				writer.WriteStartElement(XML_BAG_NAME);
				writer.WriteAttributeString("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
				writer.WriteAttributeString("xsi", "noNamespaceSchemaLocation", XmlSchema.InstanceNamespace, XSD_FILE_NAME);

				foreach (var item in bag) {
					var values = item.Properties;

					if (values == null || values.Count == 0)
						continue;

					WriteItem(writer, item.Keys, values);
				} // end of foreach.

				writer.WriteEndElement();

				writer.WriteEndDocument();

				writer.Flush();
				writer.Close();
			} // end of using.
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="writer"></param>
		/// <param name="keys"></param>
		/// <param name="values"></param>
		private static void WriteItem(XmlTextWriter writer, string[] keys, NameObjectCollection values) {
			writer.WriteStartElement(XML_ITEM_NAME);
			writer.WriteAttributeString(XML_ITEM_KEY_NAME, string.Join(",", keys));
			foreach (string propName in values.Keys) {
				if (values.IsNullOrEmpty(propName))
					continue;

				writer.WriteStartElement(XML_PROPERTY_NAME);

				writer.WriteAttributeString(XML_PROPERTY_NAME_NAME, propName);

				var v = values[propName];

				writer.WriteAttributeString(XML_PROPERTY_VALUE_NAME,
					GetValueStringFromValue(v));

				writer.WriteAttributeString(XML_PROPERTY_TYPE_NAME,
					GetTypeNameFromValue(v));

				writer.WriteEndElement();
			}
			writer.WriteEndElement();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="value"></param>
		/// <returns></returns>
		private static string GetTypeNameFromValue(object value) {
			System.Diagnostics.Debug.Assert(value != null);

			if (value is string)
				return "string";
			else if (value is int)
				return "int";
			else if (value is short)
				return "short";
			else if (value is long)
				return "long";
			else if (value is decimal)
				return "decimal";
			else if (value is DateTime)
				return "datetime";
			else if (value is float)
				return "float";
			else if (value is double)
				return "double";
			else if (value is bool)
				return "bool";
			else
				return "string";
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="value"></param>
		/// <returns></returns>
		private static string GetValueStringFromValue(object value) {
			System.Diagnostics.Debug.Assert(value != null);

			if (value is string)
				return (string)value;
			else if (value is int)
				return ((int)value).ToString("d");
			else if (value is short)
				return ((short)value).ToString("d");
			else if (value is long)
				return ((long)value).ToString("d");
			else if (value is decimal)
				return ((decimal)value).ToString("g");
			else if (value is DateTime)
				return ((DateTime)value).ToUniversalTime().ToString("u");
			else if (value is float)
				return ((float)value).ToString("r");
			else if (value is double)
				return ((double)value).ToString("r");
			else if (value is bool)
				return ((bool)value) ? "yes" : "no";
			else
				return Convert.ToString(value);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="vs"></param>
		/// <param name="typeName"></param>
		/// <returns></returns>
		private static object GetValueFromValueString(string vs, string typeName) {
			System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(typeName));

			if (string.IsNullOrEmpty(vs))
				return null;

			switch (typeName) {
				case "string":
					return vs;
				case "int":
				case "integer":
					return int.Parse(vs);
				case "short":
					return short.Parse(vs);
				case "long":
					return long.Parse(vs);
				case "money":
				case "currency":
				case "decimal":
					return decimal.Parse(vs);
				case "datetime":
				case "date":
					return DateTime.Parse(vs);
				case "float":
					return float.Parse(vs);
				case "double":
					return double.Parse(vs);
				case "bit":
				case "bool":
				case "boolean":
					vs = vs.ToLower();
					return vs == "yes" || vs == "true" || vs == "1";
				default:
					return vs;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public AutoCompleteProvider()
			: this(DEFAULT_DATA_FILE_NAME) {
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="bagFileName"></param>
		public AutoCompleteProvider(string bagFileName)
			: this(bagFileName, null) {
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="bagFileName"></param>
		/// <param name="cacheProvider"></param>
		public AutoCompleteProvider(string bagFileName, ICacheProvider cacheProvider)
			: base(cacheProvider) {
			System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(bagFileName));

			m_bagFileName = bagFileName;
			m_cacheKey = GetCacheKeyByFileName(
				System.IO.Path.GetFileName(bagFileName));
		}
	} // end of AutoCompleteProvider.
}
